package repository

import (
	"context"
	"github.com/ecodeclub/ekit/slice"
	"gorm.io/gorm"
	"time"
	"webook/internal/domain"
	"webook/internal/repository/cache"
	"webook/internal/repository/dao"
)

//go:generate mockgen -source=./article.go -package=repomocks -destination=./mocks/article.mock.go ArticleRepository
type ArticleRepository interface {
	Create(ctx context.Context, art domain.Article) (int64, error)
	Update(ctx context.Context, art domain.Article) error
	Sync(ctx context.Context, art domain.Article) (int64, error)
	SyncStatus(ctx context.Context, uid int64, id int64, status domain.ArticleStatus) error
	GetByAuthor(ctx context.Context, uid int64, offset int, limit int) ([]domain.Article, error)
	GetById(ctx context.Context, id int64) (domain.Article, error)
	GetPubById(ctx context.Context, id int64) (domain.Article, error)
	ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]domain.Article, error)
}
type CacheArticleRepository struct {
	dao   dao.ArticleDao
	cache cache.ArticleCache
	//如果直接访问userdao 就绕开了repo repo有一些缓存机制
	//userdao dao.UserDao
	userRepo UserRepository

	//v2写法 放在不同数据库里 所以采用非事务实现
	readDao   dao.ArticleReaderDao
	authorDao dao.ArticleAuthorDao

	//SyncV3  事务实现
	//缺点：没有坚持面向接口原则 跨层依赖
	db *gorm.DB
}

// ioc注入 所以要返回接口
func NewCacheArticleRepository(dao dao.ArticleDao, cache cache.ArticleCache, userRepo UserRepository) ArticleRepository {
	return &CacheArticleRepository{
		dao:      dao,
		cache:    cache,
		userRepo: userRepo,
	}
}
func (c *CacheArticleRepository) ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]domain.Article, error) {
	arts, err := c.dao.ListPub(ctx, start, offset, limit)
	if err != nil {
		return nil, err
	}
	return slice.Map[dao.PublishedArticle, domain.Article](arts,
		func(idx int, src dao.PublishedArticle) domain.Article {
			return c.toDomain(dao.Article(src))
		}), nil
}

func (c *CacheArticleRepository) GetPubById(ctx context.Context, id int64) (domain.Article, error) {
	res, err := c.cache.GetPub(ctx, id)
	if err == nil {
		return res, nil
	}
	art, err := c.dao.GetPubById(ctx, id)
	if err != nil {
		return domain.Article{}, err
	}
	//没有拿到author name 需要查询user信息，拿到创作者信息  可以访问userdao 也可以访问userrepo 后者更好
	res = c.toDomain(dao.Article(art))
	author, err := c.userRepo.FindById(ctx, art.AuthorId)
	if err != nil {
		//记录日志
		return res, nil
		//return domain.Article{},err
	}
	res.Author.Name = author.UserName
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		er := c.cache.SetPub(ctx, res)
		if er != nil {
			//记录日志
		}
	}()
	return res, nil
}
func (c *CacheArticleRepository) GetById(ctx context.Context, id int64) (domain.Article, error) {
	res, err := c.cache.Get(ctx, id)
	if err == nil {
		return res, nil
	}
	art, err := c.dao.GetById(ctx, id)
	if err != nil {
		return domain.Article{}, err
	}
	res = c.toDomain(art)
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		er := c.cache.Set(ctx, res)
		if er != nil {
			//记录日志
		}
	}()
	return res, nil
}
func (c *CacheArticleRepository) GetByAuthor(ctx context.Context, uid int64, offset int, limit int) ([]domain.Article, error) {
	//判定要不要查缓存
	//事实上 limit <=100都可以查缓存，注意返回的数据条数要满足limit条
	if offset == 0 && limit == 100 {
		res, err := c.cache.GetFirstPage(ctx, uid)
		if err == nil {
			return res, nil
		}
		//记录日志 区分缓存命中和缓存未命中 ，其中缓存未命中可以忽略
	}
	arts, err := c.dao.GetByAuthor(ctx, uid, offset, limit)
	if err != nil {
		return nil, err
	}
	res := slice.Map[dao.Article, domain.Article](arts, func(idx int, src dao.Article) domain.Article {
		return c.toDomain(src)
	})
	//也可以考虑异步写
	go func() {
		//细节 异步的话找一个新的ctx
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		if offset == 0 && limit == 100 {
			//缓存回写失败 不一定是大问题 但是有可能是大问题
			err = c.cache.SetFirstPage(ctx, uid, res)
			if err != nil {
				//记录日志 并监控这里
			}
		}
	}()
	//缓存预加载
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		c.preCache(ctx, arts)
	}()
	return res, nil
}

func (c *CacheArticleRepository) Create(ctx context.Context, art domain.Article) (int64, error) {
	id, err := c.dao.Insert(ctx, c.toEntity(art))
	if err == nil { //清理缓存 保持缓存一致性
		er := c.cache.DelFirstPage(ctx, art.Author.Id)
		if er != nil {
			//记录日志
		}
	}
	return id, err
}
func (c *CacheArticleRepository) Update(ctx context.Context, art domain.Article) error {
	err := c.dao.UpdateById(ctx, c.toEntity(art))
	if err == nil {
		er := c.cache.DelFirstPage(ctx, art.Author.Id)
		if er != nil {
			//记录日志
		}
	}
	return err
}
func (c *CacheArticleRepository) Sync(ctx context.Context, art domain.Article) (int64, error) {
	id, err := c.dao.Sync(ctx, c.toEntity(art))
	if err == nil {
		er := c.cache.DelFirstPage(ctx, art.Author.Id)
		if er != nil {
			//记录日志
		}
	}
	//尝试设置缓存
	go func() {
		//可以灵活设置过期时间 比如粉丝量多的作者 过期时间设置的长
		//需要获取用户信息
		user, err := c.userRepo.FindById(ctx, art.Author.Id)
		if err != nil {
			//记录日志
			return
		}
		art.Author = domain.Author{
			Id:   user.Id,
			Name: user.UserName,
		}
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		er := c.cache.Set(ctx, art)
		if er != nil {
			//记录日志
		}
	}()
	return id, err
}
func (c *CacheArticleRepository) SyncStatus(ctx context.Context, uid int64, id int64, status domain.ArticleStatus) error {
	err := c.dao.SyncStatus(ctx, uid, id, status.ToUint8())
	if err != nil {
		return err
	}
	err = c.cache.DelFirstPage(ctx, uid)
	if err != nil {
		//记录日志
	}
	return nil
}
func (c *CacheArticleRepository) toEntity(art domain.Article) dao.Article {
	return dao.Article{
		Id:       art.Id,
		Title:    art.Title,
		Content:  art.Content,
		AuthorId: art.Author.Id,
		//Status:   uint8(art.Status),
		Status: art.Status.ToUint8(),
	}
}
func (c *CacheArticleRepository) toDomain(art dao.Article) domain.Article {
	return domain.Article{
		Id:      art.Id,
		Title:   art.Title,
		Content: art.Content,
		Author: domain.Author{
			Id: art.AuthorId,
		},
		Ctime: time.UnixMilli(art.Ctime),
		Utime: time.UnixMilli(art.Utime),
		//Status:   uint8(art.Status),
		Status: domain.ArticleStatus(art.Status),
	}
}
func NewCacheArticleRepositoryV2(readDao dao.ArticleReaderDao, authorDao dao.ArticleAuthorDao) *CacheArticleRepository {
	return &CacheArticleRepository{
		readDao:   readDao,
		authorDao: authorDao,
	}
}
func (c *CacheArticleRepository) SyncV2(ctx context.Context, art domain.Article) (int64, error) {
	artn := c.toEntity(art)
	var id = artn.Id
	var err error
	if id > 0 {
		err = c.authorDao.Update(ctx, artn)
	} else {
		id, err = c.authorDao.Create(ctx, artn)
	}
	if err != nil {
		return 0, err
	}
	artn.Id = id
	err = c.readDao.Upsert(ctx, artn)
	return id, err
}

// 采用事务 在Repository 层面上开事务 所以应该c里有db
func (c *CacheArticleRepository) SyncV3(ctx context.Context, art domain.Article) (int64, error) {
	tx := c.db.WithContext(ctx).Begin()
	if tx.Error != nil { //事务是否开启成功
		return 0, tx.Error
	}
	//防止后面业务panic
	defer tx.Rollback()
	authorDao := dao.NewArticleORMAuthorDAO(tx)
	readerDao := dao.NewArticleORMReaderDAO(tx)

	artn := c.toEntity(art)
	var id = artn.Id
	var err error
	if id > 0 {
		err = authorDao.Update(ctx, artn)
	} else {
		id, err = authorDao.Create(ctx, artn)
	}
	if err != nil {
		return 0, err
	}
	artn.Id = id
	err = readerDao.UpsertV2(ctx, dao.PublishedArticle(artn))
	if err != nil {
		return 0, err
	}
	tx.Commit() //提交后再回滚 不会影响
	return id, nil
}

func (c *CacheArticleRepository) preCache(ctx context.Context, arts []dao.Article) {
	if len(arts) <= 0 {
		return
	}
	//有一些很长的文章也可以不缓存 平衡内存开销和性能
	const size = 1024 * 1024
	if len(arts[0].Content) >= size { //防止缓存大文档
		return
	}
	err := c.cache.Set(ctx, c.toDomain(arts[0]))
	if err != nil {
		//记录日志
	}
}
